﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-07-24 19:00
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BassDrop.Core.Common.Cloud;
using BassDrop.Core.Common.Data;
using BassDrop.Core.Common.Data.IO.Portable;
using BassDrop.Core.Common.Exceptions.Cloud;
using BassDrop.Core.Common.Plugins;
using BassDrop.Core.Common.Services;
using BassDrop.Core.Common.Utilities;
using BassDrop.Plugins.Dropbox.Localization;
using DropNetRT;
using DropNetRT.Exceptions;

namespace BassDrop.Plugins.Dropbox
{
	/// <summary>
	/// Dropbox accessor
	/// </summary>
	public class Connection : CloudConnectionBase
	{
		private const string AppKeyEncrypted = "uLcZ0haskPBNtfSzqO7AFVap0CQry679ybd0nH3LyDxFe3JrNkQFOkL9A/IBGpcEEUhWKdDOHXPgz4Uw9XI6MJIe17alG56CSIhqkSJx2nw=";
		private const string AppSecretEncrypted = "Hgbv5tCPq0WPiG9i8QOBwOgA/UAvXHj2rqcR00QKJXvzFYlFQTMYQ65NgyZgnLx3dYmRCFUwg9rHPpO/tEH9IkxQ1FG6XBsIZ5EHh8dWld4=";

		/// <summary>
		/// Returns if the provider is ready to serve requests
		/// </summary>
		public override bool IsAvailable
		{
			get
			{
				return m_client.UserLogin != null;
			}
		}

		private DropNetClient m_client;

		/// <summary>
		/// The client to handle the requests
		/// </summary>
		public DropNetClient Client
		{
			get
			{
				return m_client;
			}
		}

		/// <summary>
		/// Creates a new Dropbox connection
		/// </summary>
		/// <param name="pluginDescriptor">The descriptor object of the plugin owning this object</param>
		/// <param name="instanceName">the name of this plugin instance</param>
		public Connection( BassDropPlugin pluginDescriptor, string instanceName )
			: base( pluginDescriptor, instanceName )
		{
			try
			{
				Options = new OptionsSet( pluginDescriptor, this );
				Options.PropertyChanged += Options_PropertyChanged;

				m_client = new DropNetClient( ServiceCenter.Decrypt( AppKeyEncrypted ), ServiceCenter.Decrypt( AppSecretEncrypted ) );
				m_client.UseSandbox = true;
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Returns the account information for this cloud connection (name, quotas, etc.)
		/// </summary>
		/// <returns></returns>
		public override async Task<AccountInfo> GetAccountInfo()
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				var info = await m_client.AccountInfo();

				return new AccountInfo( info.DisplayName, info.Email, info.QuotaInfo.Quota, info.QuotaInfo.Normal + info.QuotaInfo.Shared );
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Marks a file deleted in the cloud if it exists.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		public override async Task MarkDeleted( string remotePath )
		{
			// Dropbox marks a file deleted by default
			await Delete( remotePath );
		}

		/// <summary>
		/// Marks a file not deleted in the cloud if it is marked deleted.
		/// This does not restore the file.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>did it do anything</returns>
		public override Task<bool> UnMarkDeleted( string remotePath )
		{
			// not applicable
			return ThreadingUtils.TaskFromResult( false );
		}

		/// <summary>
		/// Checks if a file is marked deleted (it has been in the cloud but since deleted). 
		/// It is possible, that a file exists yet it is marked deleted, but that generally suspects the wrong use of the API.
		/// It is also possible, that a file is deleted, yet it isn't marked so if it has been deleted with Delete() instead of
		/// MarkDeleted().
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>is the file marked as deleted</returns>
		public override async Task<bool> IsMarkedDeleted( string remotePath )
		{
			return await GetDeletionMetadata( remotePath ) != null;
		}

		/// <summary>
		/// Checks if a file is marked deleted (it has been in the cloud but since deleted) and returns the deletion metadata if it is. 
		/// It is possible, that a file exists yet it is marked deleted, but that generally suspects the wrong use of the API.
		/// It is also possible, that a file is deleted, yet it isn't marked so if it has been deleted with Delete() instead of
		/// MarkDeleted().
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>the metadata of the deleted file, shows when it was deleted or null if it isn't marked deleted</returns>
		public override async Task<FileMetadata> GetDeletionMetadata( string remotePath )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				var metadata = await m_client.GetMetaDataNoList( remotePath );

				if ( !metadata.IsDeleted )
				{
					return null;
				}

				return new FileMetadata()
				{
					LastWriteTimeUtc = metadata.UTCDateModified
				};
			}
			catch ( Exception e )
			{
				if ( e is DropboxException && ((DropboxException) e).StatusCode == HttpStatusCode.NotFound )
				{
					return null;
				}

				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Copies a file from the cloud to the local filesystem.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		protected override async Task DoGet( string remotePath, Uri localPath )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				if ( await DoExists( remotePath ) ) // TODO: megpatchelni az API-t, hogy ez ne kelljen
				{
					byte[] content = await m_client.GetFile( remotePath );  // returns the error message if something bad happens...

					using ( var stream = ServiceCenter.FileHandler.GetStream( localPath.LocalPath, PortableFileMode.Create, PortableFileAccess.Write, PortableFileShare.None ) )
					{
						await stream.WriteAsync( content, 0, content.Length );
					}
				}
				else
				{
					throw new CloudFileNotFoundException( string.Format( "404 - Remote file {0} not found.", remotePath ), null );
				}
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}

		}

		/// <summary>
		/// Copies a file from the local filesystem to the cloud.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		protected override async Task DoPut( string remotePath, Uri localPath )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				using ( var stream = ServiceCenter.FileHandler.GetStream( localPath.LocalPath, PortableFileMode.Open, PortableFileAccess.Read, PortableFileShare.Read ) )
				{
					var metadata = await m_client.Upload( ServiceCenter.FileHandler.GetDirectoryName( remotePath ), ServiceCenter.FileHandler.GetFileName( remotePath ), stream );
				}
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Checks if a file exists in the cloud.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>does the file exist</returns>
		protected override async Task<bool> DoExists( string remotePath )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				try
				{
					return !(await m_client.GetMetaDataNoList( remotePath )).IsDeleted;
				}
				catch ( DropboxException e )
				{
					if ( e.StatusCode == HttpStatusCode.NotFound )
					{
						return false;
					}
					else
					{
						throw;
					}
				}
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Returns a list of file paths matching a regular expression from baseDir.
		/// </summary>
		/// <param name="baseDir">path to the base directory of the search</param>
		/// <param name="regex">regular expression to match</param>
		/// <param name="recursive">true to search subfolders</param>
		/// <returns></returns>
		protected override async Task<List<string>> DoFind( string baseDir, Regex regex, bool recursive )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				List<string> found = new List<string>();

				try
				{
					foreach ( var item in (await m_client.GetMetaData( baseDir )).Contents )
					{
						if ( regex.IsMatch( item.Name ) )
						{
							found.Add( item.Path );
						}

						if ( recursive && item.IsDirectory )
						{
							found.AddRange( await DoFind( item.Path, regex, true ) );
						}
					}
				}
				catch ( DropboxException e )
				{
					if ( e.StatusCode != HttpStatusCode.NotFound )
					{
						throw;
					}
				}

				return found;
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Returns the metadata of the passed file
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		protected override async Task<FileMetadata> DoGetMetadata( string path )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				var metadata = await m_client.GetMetaDataNoList( path );

				if ( metadata.IsDeleted )
				{
					throw new CloudFileNotFoundException( string.Format( "404 - Remote file {0} not found.", path ), null );
				}

				return new FileMetadata()
					{
						LastWriteTimeUtc = metadata.UTCDateModified
					};
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Moves/renames a file inside the cloud. Overwriting existing files is not allowed.
		/// </summary>
		/// <param name="oldPath">the old path to the file in the cloud</param>
		/// <param name="newPath">the new (desired) path to the file in the cloud</param>
		protected override async Task DoMove( string oldPath, string newPath )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				var metadata = await m_client.Move( oldPath, newPath );
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		/// <summary>
		/// Deletes a file from the cloud if it exists.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		protected override async Task DoDelete( string remotePath )
		{
			try
			{
				if ( !IsAvailable )
				{
					throw new CloudUnavailableException( PluginStrings.ExceptionConnectionUnavailable, null, HttpStatusCode.Unauthorized );
				}

				try
				{
					var metadata = await m_client.Delete( remotePath );
				}
				catch ( DropboxException e ) // swallow 404 errors
				{
					if ( e.StatusCode != HttpStatusCode.NotFound )
					{
						throw;
					}
				}
			}
			catch ( Exception e )
			{
				throw e.GetCloudException();
			}
		}

		private void Options_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e )
		{
			if ( e.PropertyName == "UserLogin" )
			{
				m_client.SetUserToken( ((OptionsSet) Options).UserLogin );
				OnPropertyChanged( "IsAvailable" );
				OnStateChanged( "IsAvailable" );
			}
		}
	}
}
